
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_http_javascript_module_mysql.h"

static JSFunctionSpec ngx_javascript_mysql_methods[] = {
	{ "prepare", ngx_javascript_mysql_prepare, 0, 0, 0 },
	{ "execute", ngx_javascript_mysql_execute, 0, 0, 0 },
	{ "query", ngx_javascript_mysql_execute, 0, 0, 0 },
	{ "escape", ngx_javascript_mysql_escape, 0, 0, 0 },
	{ "quote", ngx_javascript_mysql_quote, 0, 0, 0 },
	{ "close", ngx_javascript_mysql_close, 0, 0, 0 },
	{ 0, 0, 0, 0, 0 }
};

static JSFunctionSpec ngx_javascript_mysql_result_methods[] = {
	{ "bind", ngx_javascript_mysql_param_bind, 0, 0, 0 },
	{ "execute", ngx_javascript_mysql_result_execute, 0, 0, 0 },
	{ "length", ngx_javascript_mysql_result_length, 0, 0, 0 },
	{ "fetchObject", ngx_javascript_mysql_result_fetchObject, 0, 0, 0 },
	{ "fetchArray", ngx_javascript_mysql_result_fetchArray, 0, 0, 0 },
	{ "fetch", ngx_javascript_mysql_result_fetchObject, 0, 0, 0 },
	{ "finish", ngx_javascript_mysql_result_finish, 0, 0, 0 },
	{ 0, 0, 0, 0, 0 }
};

static JSClass ngx_javascript_mysql_class = {
	"MySQL",
	JSCLASS_HAS_PRIVATE,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, ngx_javascript_mysql_finalize,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSClass ngx_javascript_mysql_result_class = {
	"MySQLStatement",
	JSCLASS_HAS_PRIVATE,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, ngx_javascript_mysql_result_finalize,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

static JSClass ngx_javascript_mysql_error_class = {
	"MySQLError",
	0,
	JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
	JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
	JSCLASS_NO_OPTIONAL_MEMBERS
};

JSBool ngx_javascript_init_mysql(JSContext *jcx, JSObject *global) {
	jsval result;
	if (JS_InitClass(jcx, global, NULL, &ngx_javascript_mysql_class, ngx_javascript_mysql, 0, NULL, ngx_javascript_mysql_methods, NULL, NULL) == NULL)
		return ngx_javascript_outofmemory(jcx);
	if (JS_InitClass(jcx, global, NULL, &ngx_javascript_mysql_result_class, NULL, 0, NULL, ngx_javascript_mysql_result_methods, NULL, NULL) == NULL)
		return ngx_javascript_outofmemory(jcx);
	if (JS_InitClass(jcx, global, NULL, &ngx_javascript_mysql_error_class, NULL, 0, NULL, NULL, NULL, NULL) == NULL)
		return ngx_javascript_outofmemory(jcx);
	if (JS_LookupProperty(jcx, global, "MySQL", &result) == JS_TRUE && JSVAL_IS_OBJECT(result) && !JSVAL_IS_NULL(result)) {
		JSObject *mysql_class = JSVAL_TO_OBJECT(result);
		JS_DefineProperty(jcx, mysql_class, "version", STRING_TO_JSVAL(JS_NewStringCopyN(jcx, MYSQL_SERVER_VERSION, sizeof(MYSQL_SERVER_VERSION) - 1)), NULL, NULL, 0);
		JS_DefineFunction(jcx, mysql_class, "quote", ngx_javascript_mysql_quote, 0, JSPROP_ENUMERATE);
		JS_DefineFunction(jcx, mysql_class, "escape", ngx_javascript_mysql_escape, 0, JSPROP_ENUMERATE);
	}
	return JS_TRUE;
}

JSBool ngx_javascript_mysql(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsval result;
	JSString *s;
	char *host = NULL;
	char *user = NULL;
	char *password = NULL;
	char *database = NULL;
	char *socket = NULL;
	unsigned int port = 0;
	MYSQL *db, *odb;
	if (argc == 1 && JSVAL_IS_OBJECT(argv[0]) && !JSVAL_IS_NULL(argv[0])) {
		JSObject *o = JSVAL_TO_OBJECT(argv[0]);
		if (JS_LookupProperty(jcx, o, "host", &result) == JS_TRUE) {
			s = JS_ValueToString(jcx, result);
			if (s != NULL) {
				host = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "host", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (JS_LookupProperty(jcx, o, "user", &result) == JS_TRUE) {
			s = JS_ValueToString(jcx, result);
			if (s != NULL) {
				user = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "user", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (JS_LookupProperty(jcx, o, "password", &result) == JS_TRUE) {
			s = JS_ValueToString(jcx, result);
			if (s != NULL) {
				password = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "password", JS_GetEmptyStringValue(jcx), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (JS_LookupProperty(jcx, o, "database", &result) == JS_TRUE) {
			s = JS_ValueToString(jcx, result);
			if (s != NULL) {
				database = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "database", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (JS_LookupProperty(jcx, o, "socket", &result) == JS_TRUE) {
			s = JS_ValueToString(jcx, result);
			if (s != NULL) {
				socket = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "socket", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (JS_LookupProperty(jcx, o, "port", &result) == JS_TRUE) {
			int n;
			if (JS_ValueToInt32(jcx, result, &n) == JS_TRUE) {
				port = (unsigned int) n;
				if (port != 0) {
					JS_DefineProperty(jcx, obj, "port", INT_TO_JSVAL(port), NULL, NULL, JSPROP_ENUMERATE);
				}
			}
		}
	} else {
		if (argc >= 1) {
			s = JS_ValueToString(jcx, argv[0]);
			if (s != NULL) {
				host = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "host", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (argc >= 2) {
			s = JS_ValueToString(jcx, argv[1]);
			if (s != NULL) {
				user = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "user", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (argc >= 3) {
			s = JS_ValueToString(jcx, argv[2]);
			if (s != NULL) {
				password = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "password", JS_GetEmptyStringValue(jcx), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (argc >= 4) {
			s = JS_ValueToString(jcx, argv[3]);
			if (s != NULL) {
				database = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "database", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (argc >= 5) {
			s = JS_ValueToString(jcx, argv[4]);
			if (s != NULL) {
				socket = JS_GetStringBytes(s);
				JS_DefineProperty(jcx, obj, "socket", STRING_TO_JSVAL(s), NULL, NULL, JSPROP_ENUMERATE);
			}
		}
		if (argc >= 6) {
			int n;
			if (JS_ValueToInt32(jcx, argv[5], &n) == JS_TRUE) {
				port = (unsigned int) n;
				if (port != 0) {
					JS_DefineProperty(jcx, obj, "port", INT_TO_JSVAL(port), NULL, NULL, JSPROP_ENUMERATE);
				}
			}
		}
	}
	db = odb = mysql_init(NULL);
	if (db == NULL) return ngx_javascript_outofmemory(jcx);
	mysql_options(db, MYSQL_SET_CHARSET_NAME, "utf8");
	db = mysql_real_connect(odb, host, user, password, database, port, socket, 0L);
	if (db == NULL) return ngx_javascript_mysql_throw(jcx, mysql_errno(odb), mysql_error(odb));
	JS_SetPrivate(jcx, obj, (void *) db);
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_prepare(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	return ngx_javascript_mysql_query(jcx, obj, argc, argv, rval, NGX_JAVASCRIPT_MYSQL_PREPARE);
}

JSBool ngx_javascript_mysql_execute(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	return ngx_javascript_mysql_query(jcx, obj, argc, argv, rval, NGX_JAVASCRIPT_MYSQL_EXECUTE);
}

JSBool ngx_javascript_mysql_query(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval, int todo) {
	ngx_javascript_private_t *private;
	JSObject *r;
	JSString *str;
	MYSQL *db;
	MYSQL_STMT *st;
	const jschar *sql_uc;
	const char *sql;
	jsuint len_uc, len;
	int ok;
	
	if (argc < 1) return ngx_javascript_mysql_throw(jcx, 0, "Query is empty");
	db = JS_GetPrivate(jcx, obj);
	if (db == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Connection is closed");
	private = JS_GetContextPrivate(jcx);
	if (private == NULL || private->r == NULL) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	st = mysql_stmt_init(db);
	if (st == NULL) return ngx_javascript_outofmemory(jcx);
	str = JS_ValueToString(jcx, argv[0]);
	if (str == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is empty");
	sql_uc = JS_GetStringChars(str);
	if (sql_uc == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is empty");
	len_uc = JS_GetStringLength(str);
	sql = JS_GetStringBytes(str);
	JS_EncodeCharacters(jcx, sql_uc, len_uc, NULL, &len);
	
	ok = mysql_stmt_prepare(st, sql, len);
	if (ok == 0) {
		ngx_javascript_mysql_result_t *pst = ngx_pcalloc(private->r->pool, sizeof(ngx_javascript_mysql_result_t));
		if (pst == NULL) return ngx_javascript_outofmemory(jcx);
		pst->update_max_length = 1;
		pst->cursor_type = CURSOR_TYPE_READ_ONLY;
		mysql_stmt_attr_set(st, STMT_ATTR_UPDATE_MAX_LENGTH, &pst->update_max_length);
		mysql_stmt_attr_set(st, STMT_ATTR_CURSOR_TYPE, &pst->cursor_type);
		pst->st = st;
		pst->param_count = mysql_stmt_param_count(st);
		pst->field_count = mysql_stmt_field_count(st);
		r = JS_NewObject(jcx, &ngx_javascript_mysql_result_class, NULL, NULL);
		if (r == NULL) {
			ngx_pfree(private->r->pool, pst);
			return ngx_javascript_outofmemory(jcx);
		}
		JS_SetPrivate(jcx, r, (void *) pst);
		JS_DefineProperty(jcx, r, "db", argv[-1], NULL, NULL, JSPROP_ENUMERATE);
		JS_DefineProperty(jcx, r, "sql", argv[0], NULL, NULL, JSPROP_ENUMERATE);
		*rval = OBJECT_TO_JSVAL(r);
		if (pst->param_count + pst->field_count > 0) {
			size_t size = (pst->param_count + pst->field_count) * sizeof(MYSQL_BIND) + pst->field_count * (sizeof(unsigned long) + sizeof(my_bool));
			pst->pool = ngx_pcalloc(private->r->pool, size);
			if (pst->pool == NULL) return ngx_javascript_outofmemory(jcx);
			size = 0;
			if (pst->param_count) {
				pst->bind_param = (MYSQL_BIND *) (pst->pool + size); size += pst->param_count * sizeof(MYSQL_BIND);
			}
			if (pst->field_count) {
				pst->bind_field = (MYSQL_BIND *) (pst->pool + size); size += pst->field_count * sizeof(MYSQL_BIND);
				pst->is_null = (my_bool *) (pst->pool + size); size += pst->field_count * sizeof(my_bool);
				pst->length = (unsigned long *) (pst->pool + size); size += pst->field_count * sizeof(unsigned long);
			}
		}
		if (todo == NGX_JAVASCRIPT_MYSQL_EXECUTE) {
			if (argc > 1 && ngx_javascript_mysql_param_bind(jcx, r, argc - 1, argv + 1, rval) == JS_FALSE) return JS_FALSE;
			ok = mysql_stmt_execute(st);
			if (ok != 0) return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
			ok = mysql_stmt_store_result(st);
			if (ok != 0) return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
			if (pst->field_count) {
				if (ngx_javascript_mysql_result_bind(jcx, pst) == JS_FALSE) return JS_FALSE;
			}
		}
	} else {
		return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
	}
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_result_bind(JSContext *jcx, ngx_javascript_mysql_result_t *pst) {
	int ok;
	unsigned int i = 0;
	unsigned long length;
	MYSQL_STMT *st;
	MYSQL_RES *meta;
	size_t size = 0;
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	
	if (private == NULL || private->r == NULL) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	if (pst->buffer != NULL) ngx_pfree(private->r->pool, pst->buffer);
	st = pst->st;
	meta = mysql_stmt_result_metadata(st);
	if (meta != NULL) {
		MYSQL_FIELD *fields = mysql_fetch_fields(meta);
		for (i = 0; i < pst->field_count; i ++) {
			MYSQL_FIELD *field = &fields[i];
			MYSQL_BIND *bind = pst->bind_field + i;
			if (field->length <= 1024) {
				length = field->length;
			} else {
				length = field->max_length;
			}
			size += length;
			bind->is_null = pst->is_null + i;
			bind->length = pst->length + i;
			bind->buffer_type = MYSQL_TYPE_STRING;
			bind->buffer_length = length;
		}
		if (size > 0) {
			pst->buffer = ngx_pcalloc(private->r->pool, size);
			if (pst->buffer == NULL) {
				ngx_pfree(private->r->pool, pst->pool);
				pst->pool = NULL;
				return ngx_javascript_outofmemory(jcx);
			}
			size = 0;
			for (i = 0; i < pst->field_count; i ++) {
				MYSQL_BIND *bind = &pst->bind_field[i];
				bind->buffer = ((char *) pst->buffer) + size;
				size += bind->buffer_length;
			}
		}
	}
	ok = mysql_stmt_bind_result(st, pst->bind_field);
	if (ok != 0) return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_param_bind(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	jsuint i;
	jsval result;
	MYSQL_STMT *st;
	MYSQL_BIND *bind;
	ngx_javascript_mysql_result_t *pst;
	pst = JS_GetPrivate(jcx, obj);
	if (pst == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	st = pst->st;
	if (st == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	bind = pst->bind_param;
	if (bind == NULL) return JS_TRUE;
	memset(bind, 0, pst->param_count * sizeof(MYSQL_BIND));
	if (argc < 1) return JS_TRUE;
	if (argc == 1 && JSVAL_IS_OBJECT(argv[0]) && !JSVAL_IS_NULL(argv[0])) {
		JSObject *o = JSVAL_TO_OBJECT(argv[0]);
		if (JS_IsArrayObject(jcx, o) == JS_TRUE) {
			jsuint length;
			if (JS_GetArrayLength(jcx, o, &length) == JS_TRUE) {
				// Bind params from Array
				for (i = 0; i < length; i ++) {
					if (JS_LookupElement(jcx, o, i, &result) == JS_TRUE) {
						if (ngx_javascript_mysql_param_bind_value(jcx, bind + i, result) == JS_FALSE) return JS_FALSE;
					} else {
						if (ngx_javascript_mysql_param_bind_value(jcx, bind + i, JSVAL_NULL) == JS_FALSE) return JS_FALSE;
					}
				}
			}
		} else {
			return ngx_javascript_mysql_throw(jcx, 0, "Param is not Array");
		}
	} else {
		// Bind params from arguments
		for (i = 0; i < argc; i ++) {
			if (ngx_javascript_mysql_param_bind_value(jcx, bind + i, argv[i]) == JS_FALSE) return JS_FALSE;
		}
	}
	if (mysql_stmt_bind_param(st, bind) != 0) {
		return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
	}
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_param_bind_value(JSContext *jcx, MYSQL_BIND *bind, jsval result) {
	if (JSVAL_IS_NULL(result) || JSVAL_IS_VOID(result)) {
		bind->buffer_type = MYSQL_TYPE_NULL;
	} else {
		JSString *str = JS_ValueToString(jcx, result);
		if (str == NULL) {
			bind->buffer_type = MYSQL_TYPE_NULL;
		} else {
			size_t length;
			JS_EncodeCharacters(jcx, JS_GetStringChars(str), JS_GetStringLength(str), NULL, &length);
			bind->buffer = JS_GetStringBytes(str);
			bind->buffer_type = MYSQL_TYPE_STRING;
			bind->buffer_length = length;
		}
	}
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_result_fetchArray(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	return ngx_javascript_mysql_result_fetch_value(jcx, obj, argc, argv, rval, NGX_JAVASCRIPT_MYSQL_FETCH_ARRAY);
}

JSBool ngx_javascript_mysql_result_fetchObject(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	return ngx_javascript_mysql_result_fetch_value(jcx, obj, argc, argv, rval, NGX_JAVASCRIPT_MYSQL_FETCH_OBJECT);
}

JSBool ngx_javascript_mysql_result_fetch_value(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval, int todo) {
	ngx_javascript_mysql_result_t *pst;
	MYSQL_STMT *st;
	JSObject *r;
	unsigned int i, count;
	int ok;
	
	pst = JS_GetPrivate(jcx, obj);
	if (pst == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	st = pst->st;
	if (st == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	
	*rval = JSVAL_NULL;
	if (pst->bind_field == NULL) return JS_TRUE;
	ok = mysql_stmt_fetch(st);
	if (ok != 0) return JS_TRUE;
	
	count = mysql_stmt_field_count(st);
	if (todo == NGX_JAVASCRIPT_MYSQL_FETCH_ARRAY) {
		r = JS_NewArrayObject(jcx, 0, NULL);
		if (r == NULL) return ngx_javascript_outofmemory(jcx);
		JS_SetArrayLength(jcx, r, count);
		for (i = 0; i < count; i ++) {
			JS_DefineElement(jcx, r, i, ngx_javascript_mysql_get_value(jcx, pst->bind_field + i), NULL, NULL, JSPROP_ENUMERATE);
		}
	} else {
		MYSQL_RES *re = mysql_stmt_result_metadata(st);
		MYSQL_FIELD *fields = mysql_fetch_fields(re);
		r = JS_NewObject(jcx, NULL, NULL, NULL);
		if (r == NULL) return ngx_javascript_outofmemory(jcx);
		for (i = 0; i < count; i ++) {
			JS_DefineProperty(jcx, r, fields[i].name, ngx_javascript_mysql_get_value(jcx, pst->bind_field + i), NULL, NULL, JSPROP_ENUMERATE);
		}
	}
	*rval = OBJECT_TO_JSVAL(r);
	return JS_TRUE;
}

jsval ngx_javascript_mysql_get_value(JSContext *jcx, MYSQL_BIND *bind) {
	jsval result;
	switch (bind->buffer_type) {
		default: {
			JSString *s = JS_NewStringCopyN(jcx, bind->buffer, *bind->length);
			result = STRING_TO_JSVAL(s);
		}
	}
	return result;
}

JSBool ngx_javascript_mysql_result_execute(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_mysql_result_t *pst;
	MYSQL_STMT *st;
	int ok;
	pst = JS_GetPrivate(jcx, obj);
	if (pst == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	st = pst->st;
	if (st == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	mysql_stmt_reset(st);
	if (argc > 0 && ngx_javascript_mysql_param_bind(jcx, obj, argc, argv, rval) == JS_FALSE) return JS_FALSE;
	ok = mysql_stmt_execute(st);
	if (ok != 0) return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
	ok = mysql_stmt_store_result(st);
	if (ok != 0) return ngx_javascript_mysql_throw(jcx, mysql_stmt_errno(st), mysql_stmt_error(st));
	if (pst->field_count) {
		if (ngx_javascript_mysql_result_bind(jcx, pst) == JS_FALSE) return JS_FALSE;
	}
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_result_length(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_mysql_result_t *pst;
	MYSQL_STMT *st;
	pst = JS_GetPrivate(jcx, obj);
	if (pst == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	st = pst->st;
	if (st == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Query is not prepared");
	*rval = INT_TO_JSVAL((int) mysql_stmt_num_rows(st));
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_close(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_mysql_finalize(jcx, obj);
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_result_finish(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_mysql_result_finalize(jcx, obj);
	return JS_TRUE;
}

void ngx_javascript_mysql_finalize(JSContext *jcx, JSObject *obj) {
	MYSQL *db = JS_GetPrivate(jcx, obj);
	if (db != NULL) {
		mysql_close(db);
		JS_SetPrivate(jcx, obj, NULL);
	}
}

void ngx_javascript_mysql_result_finalize(JSContext *jcx, JSObject *obj) {
	ngx_javascript_private_t *private = JS_GetContextPrivate(jcx);
	ngx_javascript_mysql_result_t *pst;
	MYSQL_STMT *st;
	pst = JS_GetPrivate(jcx, obj);
	if (private == NULL || private->r == NULL) {
		if (pst != NULL && pst->st != NULL) mysql_stmt_close(pst->st);
	} else if (pst != NULL) {
		st = pst->st;
		if (st != NULL) mysql_stmt_close(st);
		if (pst->pool != NULL) ngx_pfree(private->r->pool, pst->pool);
		if (pst->buffer != NULL) ngx_pfree(private->r->pool, pst->buffer);
		ngx_pfree(private->r->pool, pst);
	}
}

JSBool ngx_javascript_mysql_throw(JSContext *jcx, int code, const char *message) {
	jsval result;
	JSString *str;
	JSObject *error;
	const char * file;
	int line;
	if (JS_GetPendingException(jcx, &result) == JS_TRUE) return JS_FALSE;
	str = JS_NewStringCopyN(jcx, message, strlen(message));
	if (str == NULL) return JS_FALSE;
	error = JS_NewObject(jcx, &ngx_javascript_mysql_error_class, NULL, NULL);
	if (error == NULL) return JS_FALSE;
	result = STRING_TO_JSVAL(str);
	JS_DefineProperty(jcx, error, "message", result, NULL, NULL, JSPROP_ENUMERATE);
	result = INT_TO_JSVAL(code);
	JS_DefineProperty(jcx, error, "code", result, NULL, NULL, JSPROP_ENUMERATE);
	if (ngx_javascript_file_line(jcx, &file, &line)) {
		str = JS_NewStringCopyN(jcx, file, strlen(file));
		if (str != NULL) {
			result = STRING_TO_JSVAL(str);
			JS_DefineProperty(jcx, error, "file", result, NULL, NULL, JSPROP_ENUMERATE);
		}
		result = INT_TO_JSVAL(line);
		JS_DefineProperty(jcx, error, "line", result, NULL, NULL, JSPROP_ENUMERATE);
	}
	JS_SetPendingException(jcx, OBJECT_TO_JSVAL(error));
	return JS_FALSE;
}

JSBool ngx_javascript_mysql_quote(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_private_t *private;
	ngx_http_request_t *r;
	JSString *input, *output;
	jschar *old, *str;
	jsuint old_len, str_len, i, j;
	if (argc < 1) return JS_TRUE;
	private = JS_GetContextPrivate(jcx);
	if (!private) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	r = private->r;
	if (!r) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	input = JS_ValueToString(jcx, argv[0]);
	if (input == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Param is not String");
	old = JS_GetStringChars(input);
	old_len = str_len = JS_GetStringLength(input);
	for (i = 0; i < old_len; i ++) if (old[i] == 39 || old[i] == 92) str_len ++;
	str_len += 2;
	str = ngx_pnalloc(r->pool, str_len * sizeof(jschar));
	if (str == NULL) return ngx_javascript_outofmemory(jcx);
	j = 0;
	str[j] = 39; j ++;
	for (i = 0; i < old_len; i ++) {
		if (old[i] == 39 || old[i] == 92) {
			str[j] = 92; j ++;
			str[j] = old[i]; j ++;
		} else {
			str[j] = old[i]; j ++;
		}
	}
	str[j] = 39; j ++;
	output = JS_NewUCStringCopyN(jcx, str, str_len);
	ngx_pfree(r->pool, str);
	if (output == NULL) return ngx_javascript_outofmemory(jcx);
	*rval = STRING_TO_JSVAL(output);
	return JS_TRUE;
}

JSBool ngx_javascript_mysql_escape(JSContext *jcx, JSObject *obj, uintN argc, jsval *argv, jsval *rval) {
	ngx_javascript_private_t *private;
	ngx_http_request_t *r;
	JSString *input, *output;
	jschar *old, *str;
	jsuint old_len, str_len, i, j;
	if (argc < 1) return JS_TRUE;
	private = JS_GetContextPrivate(jcx);
	if (!private) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	r = private->r;
	if (!r) return ngx_javascript_throw(jcx, NGX_JAVASCRIPT_PRIVATE_ERROR);
	input = JS_ValueToString(jcx, argv[0]);
	if (input == NULL) return ngx_javascript_mysql_throw(jcx, 0, "Param is not String");
	old = JS_GetStringChars(input);
	old_len = str_len = JS_GetStringLength(input);
	for (i = 0; i < old_len; i ++) if (old[i] == 39 || old[i] == 92) str_len ++;
	str = ngx_pnalloc(r->pool, str_len * sizeof(jschar));
	if (str == NULL) return ngx_javascript_outofmemory(jcx);
	j = 0;
	for (i = 0; i < old_len; i ++) {
		if (old[i] == 39 || old[i] == 92) {
			str[j] = 92; j ++;
			str[j] = old[i]; j ++;
		} else {
			str[j] = old[i]; j ++;
		}
	}
	output = JS_NewUCStringCopyN(jcx, str, str_len);
	ngx_pfree(r->pool, str);
	if (output == NULL) return ngx_javascript_outofmemory(jcx);
	*rval = STRING_TO_JSVAL(output);
	return JS_TRUE;
}
